feat(workflow): identity-safe assign_agent action (Slice 1) - #6060
Open
mfethe1 wants to merge 2 commits into
Open
feat(workflow): identity-safe assign_agent action (Slice 1)#6060mfethe1 wants to merge 2 commits into
mfethe1 wants to merge 2 commits into
Conversation
Replaces prose-inferred agent dispatch (via `send_message` reverse-parsing
of `@Name` mentions) with a pubkey-addressed workflow action. Two failure
modes of the mention path are structural: (a) two channel members sharing
a display name make the mention ambiguous and wake no one; (b) a rename
silently rewrites the target. `assign_agent` binds dispatch to the
target's hex pubkey and membership-checks at emit time.
Contract per Airy's Slice-1 review:
- Singular `agent_pubkey` — group fan-out is a separate explicit step.
- The relay sink emits exactly two `p` tags: workflow owner (attribution)
and `agent_pubkey` (wake). Text is NEVER reverse-parsed for `@Name`.
- Fail-closed: `AssigneeNotMember` if the agent is not a channel member.
- Schema accepts a static 64-hex pubkey OR a single `{{...}}` template
placeholder (e.g. `{{trigger.author}}`); mixed literal+template strings
are rejected so a stray name cannot smuggle an identity in. The resolved
value is re-validated as 64-char lowercase hex at dispatch time.
- No default `ActionSink` impls; the new `assign_agent` method is a hard
compile-time obligation on any implementor.
Tests: 11 schema tests (parse/round-trip/hex-shape rejects), 3 executor
template-resolution tests, 2 postgres-gated relay-sink integration tests
covering the duplicate-name repro (two "Winnie" members, only the
selected pubkey wakes) and the non-member fail-closed path.
Non-goals for this slice (per Airy): task leases, reviewer-independence,
approval suspend/resume, run-event emission. Run history is addressed
separately by wiring the CLI to the existing DB-backed
`GET /workflows/{id}/runs` endpoint in a follow-up PR.
Signed-off-by: Michael Feth <michael@jira-flow.com>
themiguelamador
suggested changes
Aug 16, 2026
themiguelamador
left a comment
There was a problem hiding this comment.
Requesting changes. The identity-safe action is the right direction, but the submitted head does not yet preserve its advertised single-assignee and template contracts.
Findings:
- P1 — owner attribution can wake a second agent. ACP treats every
ptag as a wake target. Emittingp(owner)plusp(assignee)therefore wakes the workflow owner too whenever the owner is a managed agent. Owner attribution must use Buzz's relay-trustedactortag, leaving exactly oneptag for the assignee. - P1 — templated routing fields cannot be saved, and resolved task IDs are not checked. The executor resolves templates in
channelandtask_id, but schema validation rejects those same templates. The executor test bypassesWorkflowDef::validateand uses a non-UUID event ID as a task ID, masking the mismatch. A resolvedtask_idcan consequently reach the sink without the UUID guarantee documented by the action. - P2 — identity shape checks accept values the runtime rejects. Both the pubkey and single-template validators trim only for validation, then retain the padded original value. Inputs such as
" <64-hex>"and" {{trigger.author}}"save successfully and fail later. The template check also accepts malformed nested/triple-brace forms. - P2 — the public sink boundary trusts malformed correlation IDs. A direct
ActionSink::assign_agentcaller can emit a non-UUIDtasktag (and the submitted implementation silently drops an empty one), despite the public contract claiming a UUID. The sink needs its own validation/canonicalization. - P2 — action failures are reported as
webhook_failed.ActionSinkErrormaps toWorkflowError::WebhookError, so a removed assignee or invalid assignment is persisted with the wrong stable error code. Database failures also need to retain their database classification. - P2 — assignment text is copied into an info log. Task content may contain sensitive incident/customer data; the new action should log routing metadata without duplicating the full message body.
- P3 — diagnostics and docs describe the wrong action/contract. Shared channel resolution reports
SendMessageforAssignAgent; docs claim exactly twoptags even though the implementation deduplicates owner=assignee; and theparse_all_action_typescoverage omits the new variant. - P3 — PR metadata violates this repository's attribution rule. The
Generated with Claude Codefooter must be removed. I attempted to remove only that footer, but the reviewer account cannot edit another author's PR description.
I prepared the complete fix as 626849e7 on Complear:review/pr-6060-fix. It uses actor for attribution and a sole assignee p tag, aligns definition/runtime validation for all templated identity fields, validates UUIDs at both executor and sink boundaries, preserves action/database error classifications, removes message content from the new info log, and adds regression coverage.
Verification on the fix:
cargo test -p buzz-workflow --lib: 173 passed, 2 Postgres-gated ignoredcargo test -p buzz-relay --lib workflow_sink: 18 passed, 3 Postgres-gated ignoredcargo test -p buzz-relay --lib workflow_sink -- --ignored --test-threads=1against an isolated fully migrated database: 3 passed- strict clippy for
buzz-workflow+buzz-relay: passed cargo doc -p buzz-workflow --no-deps: passed- full relay library suite: 878 passed; its two unrelated global-state/timing failures both passed on exact isolated rerun
- formatting and
git diff --check: passed
parse_all_action_types exists to catch exactly this and was passing green while blind to the new variant: it asserted steps.len() == 7 over a fixture that never included assign_agent, so an 8th action type could ship without the test noticing. Extended the fixture to 8 steps, bumped the assertion, and added the matches! arm. ARCHITECTURE.md still read '7 action types' above a 7-row table. Added the row and corrected the count. Both are the same pattern: a new entry in a closed enumeration that lives in several places the compiler does not check. 'git grep -ln <new-variant>' should return more than the files you edited. Verified: cargo test -p buzz-workflow 169 passed / 0 failed; cargo fmt clean. Co-authored-by: Michael Feth <michael@jira-flow.com> Signed-off-by: Michael Feth <michael@jira-flow.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an identity-safe
assign_agentworkflow action so agent dispatch binds to an immutable hex pubkey instead of a reverse-parsed@Namemention. Two structural failure modes of the mention path go away: (a) two channel members sharing a display name silently wake no one; (b) a rename silently rewrites the target.6e7044ac62d81d98aa7560dccfef3b093bee879dd8281b9c93395f15d55091b131bb2747a0a3da8a(one commit behindorigin/main@f956e6fe, no conflict)4108b496-0efb-4fc6-85e3-6c88defb467c(autonomous-collaboration Slice 1)Review status
There is an open
CHANGES_REQUESTEDagainst this exact head (review4946833713, @themiguelamador) that has not yet been answered. The "Known gaps" section below folds in its findings so the diff and the body agree; a prepared fix exists atComplear:review/pr-6060-fix(626849e7). Do not read the sections below as review-clean.What ships
crates/buzz-workflow/src/schema.rs) — newActionDef::AssignAgent { agent_pubkey, text, channel?, task_id? }, plus a newvalidate_actionhook called fromWorkflowDef::validatefor every step. Definition-time validation accepts either a static 64-char lowercase hexagent_pubkeyor a single{{...}}template placeholder (e.g.{{trigger.author}}); mixed literal+template strings such asprefix-{{trigger.author}}are rejected so a stray name cannot smuggle an identity in.textmust be non-empty.channelandtask_idmust parse as UUIDs when set.crates/buzz-workflow/src/action_sink.rs) — extends theActionSinktrait withassign_agent(community_id, channel_id, text, author_pubkey, agent_pubkey, task_id)and addsActionSinkError::AssigneeNotMember. Deliberately no default implementation:RelayActionSinkis the only implementor in the tree (git grep -n "impl ActionSink" -- '*.rs'returns exactly one hit), so a missing impl is a compile error rather than a runtimeunimplemented!.crates/buzz-workflow/src/executor.rs) — newAssignAgentarms inresolve_step_templatesanddispatch_action. The resolvedagent_pubkeyis re-validated as 64-char lowercase hex before dispatch, so a misspelled template variable (whichresolve_templatepasses through as literal{{...}}) fails the run loudly instead of misrouting.crates/buzz-relay/src/workflow_sink.rs) —RelayActionSink::assign_agentresolves the tenant, validates text and channel UUID, rejects archived channels, checks the workflow owner's channel access, and membership-checks the assignee, returningAssigneeNotMemberfail-closed if they are not in the channel. It then signs akind:9(KIND_STREAM_MESSAGE) event carrying an owner attributionptag, an assigneeptag (deduplicated when owner == assignee, so the tag count is one or two), anhchannel tag,buzz:workflow, and an optionaltaskcorrelation tag. The message text is never scanned for@Name.Known gaps at this head
Carried from the open review; listed here so the body matches the diff rather than the intent.
ptag matching an agent's pubkey (crates/buzz-acp/src/filter.rs:390). Emittingp(owner)alongsidep(assignee)therefore wakes a managed-agent owner too, which qualifies the single-wake premise. This is inherited fromsend_message, which already emits the same attribution tag; the suggested fix is to move owner attribution to the relay-trustedactortag (already recognized atcrates/buzz-relay/src/handlers/ingest.rs:889). Whether to changesend_messageat the same time is an open call.channel/task_idare resolved but cannot be saved.resolve_step_templatestemplates both fields, whilevalidate_actionrequires them to parse as UUIDs — which no{{...}}string does. The executor testassign_agent_resolves_text_and_task_id_templatesdemonstrates the templated path only because it constructs aStepdirectly and never callsWorkflowDef::validate. Either the validator should accept a single template placeholder (with a resolved-value UUID re-check in the executor) or the templating should be dropped from those two fields. This also diverges fromsend_message, which still accepts a templatedchannel.is_lowercase_hex_pubkeyandis_single_templateboth.trim()for the check whilevalidate_actionnever writes the normalized value back, so" <64-hex>"and" {{trigger.author}}"save cleanly and fail at dispatch.is_single_templatealso accepts the malformed{{{x}}}form.task_id. The trait doc claims "the executor performs shape validation before calling", but the executor re-validates onlyagent_pubkey. A directActionSink::assign_agentcaller can emit a non-UUIDtasktag, and a whitespace-onlytask_idis silently dropped rather than rejected.From<ActionSinkError> for WorkflowErrormaps every variant toWorkflowError::WebhookError, soAssigneeNotMemberand DB failures both persist aswebhook_failed. Untouched by this PR, but the new variant is the first one for which the collapse is clearly wrong.resolve_send_message_channelhardcodes"SendMessage:"in all four of its error strings, whichAssignAgentnow shares."AssignAgent → {channel}: {text}"), mirroringsend_message. Task content may carry incident or customer detail.Registry mirrors not yet updated
git grep -ln "assign_agent\|AssignAgent"at this head returns only the four changed files. Three mirrors of the action enumeration are therefore stale:ARCHITECTURE.md:534**7 action types:**with a 7-row table, byte-identical toorigin/maincrates/buzz-workflow/src/schema.rs:456parse_all_action_typessteps.len() == 7; passes, so the gap is silentdesktop/src/features/workflows/ui/workflowFormTypes.ts:12ACTION_TYPESis a 7-element closed list; line 256 rejects unknown actions withUnsupported action type "…" — use the YAML editor, so anassign_agentworkflow bounces the whole definition out of the form editorThe desktop form-editor gap degrades gracefully (YAML editor still works) and can reasonably be a follow-up; the doc and test updates should land here.
Non-goals for this slice
Task leases/claims, reviewer-independence enforcement, exact-head binding, availability-aware reassignment, hop/budget/terminal-state protocol, workflow approval suspend/resume (WF-08). No run-event emission or
#runfiltering — Nostr generic-tag filters are single-letter and#runis not valid; run history will be wired to the existing DB-backedGET /workflows/{workflow_id}/runsendpoint in a follow-up. No change to PR #5983 lanes. No mobile or CLI surface.Diff shape
crates/buzz-workflow/src/schema.rsvalidate_action+ 3 shape predicates + 11 testscrates/buzz-workflow/src/action_sink.rsAssigneeNotMembervariantcrates/buzz-workflow/src/executor.rscrates/buzz-relay/src/workflow_sink.rsFour files, 884 insertions, 0 deletions.
Verification
Repo CI has not run on this head. All three workflows (
CI,Docker image,Desktop Release Candidate) sit ataction_requiredpending maintainer approval;DCO Checkis the only executed check and it passes. Everything below is a local result on6e7044acand needs CI confirmation.cargo fmt -p buzz-workflow -p buzz-relay -- --check— cleancargo clippy -p buzz-workflow -p buzz-relay --tests --all-features -- -D warnings— cleancargo test -p buzz-workflow --lib— 169 passed, 0 failed, 2 ignoredcargo test -p buzz-relay --lib workflow_sink— 17 passed, 0 failed, 3 ignored (2 new + 1 pre-existing Postgres-gated)cargo test -p buzz-relay --lib workflow_sink -- --ignored) — authored, not executed; no accessible local Postgres. Covers the duplicate-name repro (twoWinniemembers, only the selected pubkey isp-tagged) and the non-member fail-closed path.assign_agentstep targeting one of two same-name members, trigger it, confirm exactly one wake.Two
buzz-relay --libfailures (api::git::policy::tests::bash_hmac_matches_rust_hmac,api::admin::tests::feedback_attachment_rejects_unknown_feedback) reproduce on the cleand8281b9cbase and are unrelated to this change.